/*
 * Copyright 2007-2011 the original author or authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.hxrk.cecardvr;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;

/**
 * This is the graphical object used to display a real-time preview of the Camera.
 * It MUST be an extension of the {@link SurfaceView} class.<br />
 * It also needs to implement some other interfaces like {@link SurfaceHolder.Callback}
 * (to react to SurfaceView events)
 *
 * @author alessandrofrancesconi, hunghd
 */
public class CvUSBPreview extends SurfaceView implements SurfaceHolder.Callback, PreviewCallback {
    private static final int STOPPED = 0;
    private static final int STARTED = 1;

    public static final int CAMERA_BACK = 99;
    public static final int CAMERA_FRONT = 98;

    public static final int SCALE_FIT = 1;
    public static final int SCALE_FULL = 2;

    private static final int MAGIC_TEXTURE_ID = 10;

    /**
     * ASPECT_RATIO_W and ASPECT_RATIO_H define the aspect ratio
     * of the Surface. They are used when {@link #onMeasure(int, int)}
     * is called.
     */
    private final float ASPECT_RATIO_W = 4.0f;
    private final float ASPECT_RATIO_H = 3.0f;

    /**
     * The "holder" is the underlying surface.
     */
    private SurfaceHolder surfaceHolder;

    protected boolean enabled = true;
    private boolean surfaceExist;
    private CvCameraViewListener listener;
    private int state = STOPPED;
    private final Object syncObject = new Object();
    private int cameraId = -1;

//    private CamParameter camParameter;

    private int scaleType = SCALE_FIT;

    private boolean stopThread = false;
    private Thread thread;

    // 1小画面  2 全屏画面
    private int TYPE = 1;

    public CvUSBPreview(Context context, AttributeSet attrs) {
        super(context, attrs);

        this.surfaceHolder = this.getHolder();
        this.surfaceHolder.addCallback(this);

        // deprecated setting, but required on Android versions prior to API 11
        if (Build.VERSION.SDK_INT < 11) {
            this.surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        }
    }

    public CvUSBPreview(Context context, int camType, int scaleType) {
        super(context);

        this.surfaceHolder = this.getHolder();
        this.surfaceHolder.addCallback(this);

        // deprecated setting, but required on Android versions prior to API 11
        if (Build.VERSION.SDK_INT < 11) {
            this.surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        }
    }

    public void setCvCameraViewListener(CvCameraViewListener listener) {
        this.listener = listener;
    }

    public void setCvCameraType(int type) {
        this.TYPE = type;
    }

    public int getCameraId() {
        return cameraId;
    }

    /**
     * Called when the surface is created for the first time. It sets all the
     * required {@link #cameraDevice}'s parameters and starts the preview stream.
     *
     * @param holder
     */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        /* Do nothing. Wait until surfaceChanged delivered */
    }

    /**
     * [IMPORTANT!] A SurfaceChanged event means that the parent graphic has changed its layout
     * (for example when the orientation changes). It's necessary to update the {@link CvUSBPreview}
     * orientation, so the preview is stopped, then updated, then re-activated.
     *
     * @param holder The SurfaceHolder whose surface has changed
     * @param format The new PixelFormat of the surface
     * @param w      The new width of the surface
     * @param h      The new height of the surface
     */
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        if (this.surfaceHolder.getSurface() == null) {
            LogUtils.e("surfaceChanged(): surfaceHolder is null, nothing to do.");
            return;
        }

        synchronized (syncObject) {
            if (!surfaceExist) {
                surfaceExist = true;
                checkCurrentState();
            } else {
                /** Surface changed. We need to stop camera and restart with new parameters */
                /* Pretend that old surface has been destroyed */
                surfaceExist = false;
                checkCurrentState();
                /* Now use new surface. Say we have it now */
                surfaceExist = true;
                checkCurrentState();
            }
        }
    }

    /**
     * Called when syncObject lock is held
     */
    private void checkCurrentState() {
        LogUtils.d("call checkCurrentState");
        int targetState;

        if (enabled && surfaceExist && getVisibility() == VISIBLE) {
            targetState = STARTED;
        } else {
            targetState = STOPPED;
        }

        if (targetState != state) {
            /* The state change detected. Need to exit the current state and enter target state */
            processExitState(state);
            state = targetState;
            processEnterState(state);
        }
    }

    private void processExitState(int state) {
        LogUtils.d("call processExitState: " + state);
        switch (state) {
            case STARTED:
                onExitStartedState();
                break;
            case STOPPED:
                onExitStoppedState();
                break;
        }
        ;
    }

    private void onExitStartedState() {
        disconnectCamera();
    }

    private void disconnectCamera() {
        /* 1. We need to stop thread which updating the frames
         * 2. Stop camera and release it
         */
        LogUtils.d("Disconnecting from camera");
        try {
            stopThread = true;
            LogUtils.d("Notify thread");
            synchronized (this) {
                this.notify();
            }
            LogUtils.d("Wating for thread");
            if (thread != null)
                thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            thread = null;
        }

        /* Now release camera */
//        releaseCamera();
    }

    private void processEnterState(int state) {
        LogUtils.d("call processEnterState: " + state);
        switch (state) {
            case STARTED:
//                initUSBCamera();
                stopThread = false;
                thread = new Thread(new CameraWorker());
                thread.start();
                break;
            case STOPPED:
                onEnterStoppedState();
                if (listener != null) {
                    listener.onCameraViewStopped();
                }
                break;
        }
    }

    private void onEnterStoppedState() {
        /* nothing to do */
    }

    private void onExitStoppedState() {
        /* nothing to do */
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        LogUtils.d("surfaceDestroyed");
        synchronized (syncObject) {
            surfaceExist = false;
            checkCurrentState();
        }
    }

//    /**
//     * [IMPORTANT!] Probably the most important method here. Lots of users experience bad
//     * camera behaviors because they don't override this guy.
//     * In fact, some Android devices are very strict about the size of the surface
//     * where the preview is printed: if its ratio is different from the
//     * original one, it results in errors like "startPreview failed".<br />
//     * This methods takes care on this and applies the right size to the
//     * {@link CvUSBPreview}.
//     *
//     * @param widthMeasureSpec  horizontal space requirements as imposed by the parent.
//     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
//     */
//    @Override
//    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        int height = MeasureSpec.getSize(heightMeasureSpec);
//        int width = MeasureSpec.getSize(widthMeasureSpec);
//
//        // do some ultra high precision math...
//        float expectedRatio = height > width ? ASPECT_RATIO_H / ASPECT_RATIO_W : ASPECT_RATIO_W / ASPECT_RATIO_H;
//        float screenRatio = width * 1f / height;
//        if (screenRatio > expectedRatio) {
//            if (scaleType == SCALE_FULL) {
//                height = (int) (width / expectedRatio + .5);
//            } else {
//                width = (int) (height * expectedRatio + .5);
//            }
//        } else {
//            if (scaleType == SCALE_FULL) {
//                width = (int) (height * expectedRatio + .5);
//            } else {
//                height = (int) (width / expectedRatio + .5);
//            }
//        }
//
//        setMeasuredDimension(width, height);
//        LogUtils.i("onMeasure(): set surface dimension to " + width + "x" + height);
//    }

    private boolean initUSBCamera() {
//        synchronized (this) {
//            if (this.camParameter != null) {
//                // do the job only if the camera is not already set
//                LogUtils.i("initUSBCamera(): camera is already set, nothing to do");
//                return true;
//            }
//
//            if (Build.VERSION.SDK_INT >= 9) {
//                while (camParameter == null) {
//                    for (int i = 0; i < CamService.CamNumber; i++) {
//                        if ((CamService.camParameters[i] != null && CamService.camParameters[i].IMG_TYPE == CamParameter.FMT_TYPE_H264 && CamService.camParameters[i].DEV_TYPE == CamParameter.DEV_TYPE_USB)) {
//                            this.camParameter = CamService.camParameters[i];
//                            this.cameraId = camParameter.CamID;
//                            break;
//                        }
//                    }
//                }
//            }
//        }

        return true;
    }

    @Override
    public void onPreviewFrame(byte[] raw, Camera cam) {

    }


    /**
     * This class interface is abstract representation of single frame from camera for onCameraFrame callback
     * Attention: Do not use objects, that represents this interface out of onCameraFrame callback!
     */
    public interface CvCameraViewFrame {

        /**
         * This method returns RGBA Mat with frame
         */
        public Mat rgba();

        /**
         * This method returns single channel gray scale Mat with frame
         */
        public Mat gray();
    }

    ;

    Rect destRect = new Rect();

    /**
     * This method shall be called by the subclasses when they have valid
     * object and want it to be delivered to external client (via callback) and
     * then displayed on the screen.
     *
     * @param frame - the current frame to be delivered
     */
    protected void deliverAndDrawFrame(CamParameter camParameter, CamParameter.JavaCameraFrame frame) {
        Mat s;
        if (listener != null) {
            s = listener.onCameraFrame(frame);
        } else {
            s = frame.rgba();
        }

        boolean bmpValid = false;
        Bitmap mCacheBitmap = camParameter.bmp;
        if (s != null) {
            try {
                Utils.matToBitmap(s, mCacheBitmap);
                bmpValid = true;
            } catch (Exception e) {
                LogUtils.e("deliverAndDrawFrame", "Mat type: " + s);
                LogUtils.e("deliverAndDrawFrame", "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());
                LogUtils.e("deliverAndDrawFrame", "Utils.matToBitmap() throws an exception: " + e.getMessage());
                bmpValid = false;
            }
        }

        if (bmpValid && mCacheBitmap != null) {
            Canvas canvas = getHolder().lockCanvas();
            if (canvas != null) {
                Rect srcRect = getHolder().getSurfaceFrame();
                int left = 0;
                int top = (srcRect.bottom / 2) / 2;
                int right = srcRect.right / 2;
                int bottom = srcRect.bottom / 2 + top;
                destRect.set(left, top, right, bottom);

                canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);
                if (TYPE == 2) {
                    canvas.drawBitmap(mCacheBitmap,
                            new Rect(0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
                            srcRect, null);
                } else {
                    canvas.drawBitmap(mCacheBitmap,
                            new Rect(0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
                            destRect, null);
                }

                getHolder().unlockCanvasAndPost(canvas);
            }
        }

        if (s != null) {
            s.release();
        }
    }

    private class CameraWorker implements Runnable {
        public void run() {
            if (Build.VERSION.SDK_INT >= 9) {
                while (surfaceExist){
                    for (int i = 0; i < CamService.CamNumber; i++) {
                        if ((CamService.camParameters[i] != null && CamService.camParameters[i].IMG_TYPE == CamParameter.FMT_TYPE_H264 && CamService.camParameters[i].DEV_TYPE == CamParameter.DEV_TYPE_USB)) {
                            CamParameter camParameter = CamService.camParameters[i];
                            cameraId = camParameter.CamID;

                            if (listener != null) {
                                listener.onCameraViewStarted(camParameter.IMG_WIDTH, camParameter.IMG_HEIGHT);
                            }

                            boolean isShow = false;
                            do {
                                if (!isShow && camParameter != null){
                                    synchronized (camParameter.datasyn){
                                        if (camParameter.mCameraFrame[0] != null)
                                            isShow = true;
                                    }

                                    try {
                                        Thread.sleep(50);
                                    } catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
                                }

                                if (isShow){
                                    synchronized (camParameter.videosyn){
                                        deliverAndDrawFrame(camParameter, camParameter.mCameraFrame[0]);
                                    }
                                }

//                                boolean hasFrame = false;
//                                if (camParameter.cameraFrameReady[0]) {
//                                    hasFrame = true;
//                                }
//
//                                if (!stopThread && hasFrame) {
////                                  if (!camParameter.mFrameChain[0].empty()) {
//                                    deliverAndDrawFrame(camParameter, camParameter.mCameraFrame[0]);
////                                  }
//                                }
//                                camParameter.cameraFrameReady[0] = false;
                            } while (!stopThread);

                            break;
                        }
                    }
                }
            }
            LogUtils.d("Finish processing thread");
        }
    }

    public interface CvCameraViewListener {
        /**
         * This method is invoked when camera preview has started. After this method is invoked
         * the frames will start to be delivered to client via the onCameraFrame() callback.
         *
         * @param width  -  the width of the frames that will be delivered
         * @param height - the height of the frames that will be delivered
         */
        public void onCameraViewStarted(int width, int height);

        /**
         * This method is invoked when camera preview has been stopped for some reason.
         * No frames will be delivered via onCameraFrame() callback after this method is called.
         */
        public void onCameraViewStopped();

        /**
         * This method is invoked when delivery of the frame needs to be done.
         * The returned values - is a modified frame which needs to be displayed on the screen.
         * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
         */
        public Mat onCameraFrame(CvCameraViewFrame inputFrame);
    }
}
